/**
 * Jin - a chess client for internet chess servers.
 * More information is available at http://www.jinchess.com/.
 * Copyright (C) 2002, 2003 Alexander Maryanovsky.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package free.jin.freechess;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.Socket;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.SwingUtilities;

import free.chess.*;
import free.chess.variants.BothSidesCastlingVariant;
import free.chess.variants.NoCastlingVariant;
import free.chess.variants.atomic.Atomic;
import free.chess.variants.fischerrandom.FischerRandom;
import free.chess.variants.suicide.Suicide;
import free.chessclub.ChessclubConnection;
import free.freechess.*;
import free.jin.*;
import free.jin.event.*;
import free.jin.freechess.event.IvarStateChangeEvent;
import free.util.Pair;
import free.util.TextUtilities;


/**
 * An implementation of the JinConnection interface for the freechess.org
 * server.
 */

public class JinFreechessConnection extends FreechessConnection implements Connection,
        SeekConnection, PGNConnection {


    /**
     * Our listener manager.
     */

    private final FreechessListenerManager listenerManager = new FreechessListenerManager(this);


    /**
     * Creates a new JinFreechessConnection with the specified hostname, port,
     * requested username and password.
     */

    public JinFreechessConnection(String requestedUsername, String password) {
        super(requestedUsername, password, System.out);

        setInterface(Jin.getInstance().getAppName() + " " + Jin.getInstance().getAppVersion() +
                " (" + System.getProperty("java.vendor") + " " + System.getProperty("java.version") +
                ", " + System.getProperty("os.name") + " " + getSafeOSVersion() + ")");

        setStyle(12);

        setIvarState(Ivar.GAMEINFO, true);
        setIvarState(Ivar.SHOWOWNSEEK, true);
        setIvarState(Ivar.PENDINFO, true);
        setIvarState(Ivar.MOVECASE, true);
        // setIvarState(Ivar.COMPRESSMOVE, true); Pending DAV's bugfixing spree
        setIvarState(Ivar.LOCK, true);
    }


    /**
     * Returns the OS version after stripping out the patch level from it.
     * We do this to avoid revealing that information to everyone on the server.
     */

    private static String getSafeOSVersion() {
        String osVersion = System.getProperty("os.version");
        int i = osVersion.indexOf(".", osVersion.indexOf(".") + 1);
        if (i != -1)
            osVersion = osVersion.substring(0, i) + ".x";

        return osVersion;
    }


    /**
     * Returns a Player object corresponding to the specified string. If the
     * string is "W", returns <code>Player.WHITE</code>. If it's "B", returns
     * <code>Player.BLACK</code>. Otherwise, throws an IllegalArgumentException.
     */

    public static Player playerForString(String s) {
        if (s.equals("B"))
            return Player.BLACK_PLAYER;
        else if (s.equals("W"))
            return Player.WHITE_PLAYER;
        else
            throw new IllegalArgumentException("Bad player string: " + s);
    }


    /**
     * Returns our ListenerManager.
     */

    public ListenerManager getListenerManager() {
        return getFreechessListenerManager();
    }


    /**
     * Returns out ListenerManager as a reference to FreechessListenerManager.
     */

    public FreechessListenerManager getFreechessListenerManager() {
        return listenerManager;
    }


    /**
     * Fires an "attempting" connection event and invokes {@link free.util.Connection#initiateConnect(String, int)}.
     */

    public void initiateConnectAndLogin(String hostname, int port) {
        listenerManager.fireConnectionAttempted(this, hostname, port);

        initiateConnect(hostname, port);
    }


    /**
     * Fires an "established" connection event.
     */

    protected void handleConnected() {
        listenerManager.fireConnectionEstablished(this);

        super.handleConnected();
    }


    /**
     * Fires a "failed" connection event.
     */

    protected void handleConnectingFailed(IOException e) {
        listenerManager.fireConnectingFailed(this, e.getMessage());

        super.handleConnectingFailed(e);
    }


    /**
     * Fires a "login succeeded" connection event and performs other on-login tasks.
     */

    protected void handleLoginSucceeded() {
        super.handleLoginSucceeded();

        sendCommand("$set bell 0");
        filterLine("Bell off.");

        listenerManager.fireLoginSucceeded(this);
    }


    /**
     * Fires a "login failed" connection event.
     */

    protected void handleLoginFailed(String reason) {
        listenerManager.fireLoginFailed(this, reason);

        super.handleLoginFailed(reason);
    }


    /**
     * Fires a "connection lost" connection event.
     */

    protected void handleDisconnection(IOException e) {
        listenerManager.fireConnectionLost(this);

        super.handleDisconnection(e);
    }


    /**
     * Overrides {@link free.util.Connection#connectImpl(String, int)} to return a timesealing socket.
     */

    protected Socket connectImpl(String hostname, int port) throws IOException {
        Socket result = null;
        try {
            Class tsSocketClass = Class.forName("free.freechess.timeseal.TimesealingSocket");
            Constructor tsSocketConstructor = tsSocketClass.getConstructor(new Class[]{String.class, int.class});
            result = (Socket) tsSocketConstructor.newInstance(new Object[]{hostname, new Integer(port)});
        } catch (ClassNotFoundException e) {
        }
        catch (SecurityException e) {
        }
        catch (NoSuchMethodException e) {
        }
        catch (IllegalArgumentException e) {
        }
        catch (InstantiationException e) {
        }
        catch (IllegalAccessException e) {
        }
        catch (InvocationTargetException e) {
            Throwable targetException = e.getTargetException();
            if (targetException instanceof IOException)
                throw (IOException) targetException;
            else if (targetException instanceof RuntimeException)
                throw (RuntimeException) targetException;
            else if (targetException instanceof Error)
                throw (Error) targetException;
            else
                e.printStackTrace(); // Shouldn't happen, I think
        }

        if (result == null)
            result = new Socket(hostname, port);

        return result;
    }


    /**
     * Notifies any interested PlainTextListener of the received line of otherwise
     * unidentified text.
     */

    protected void processLine(String line) {
        listenerManager.firePlainTextEvent(new PlainTextEvent(this, line));
    }


    /**
     * Gets called when the server notifies us of a change in the state of some
     * ivar.
     */

    protected boolean processIvarStateChanged(Ivar ivar, boolean state) {
        IvarStateChangeEvent evt = new IvarStateChangeEvent(this, ivar, state);

        listenerManager.fireIvarStateChangeEvent(evt);

        return false;
    }


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processPersonalTell(String username, String titles, String message) {
        listenerManager.fireChatEvent(new ChatEvent(this, "tell", ChatEvent.PERSON_TO_PERSON_CHAT_CATEGORY,
                username, (titles == null ? "" : titles), -1, message, null));

        return true;
    }


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processSayTell(String username, String titles, int gameNumber, String message) {
        listenerManager.fireChatEvent(new ChatEvent(this, "say", ChatEvent.PERSON_TO_PERSON_CHAT_CATEGORY,
                username, (titles == null ? "" : titles), -1, message, new Integer(gameNumber)));

        return true;
    }


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processPTell(String username, String titles, String message) {
        listenerManager.fireChatEvent(new ChatEvent(this, "ptell", ChatEvent.PERSON_TO_PERSON_CHAT_CATEGORY,
                username, (titles == null ? "" : titles), -1, message, null));

        return true;
    }


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processChannelTell(String username, String titles, int channelNumber,
                                         String message) {

        listenerManager.fireChatEvent(new ChatEvent(this, "channel-tell", ChatEvent.ROOM_CHAT_CATEGORY,
                username, (titles == null ? "" : titles), -1, message, new Integer(channelNumber)));

        return true;
    }


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processKibitz(String username, String titles, int rating, int gameNumber,
                                    String message) {

        if (titles == null)
            titles = "";

        listenerManager.fireChatEvent(new ChatEvent(this, "kibitz", ChatEvent.GAME_CHAT_CATEGORY,
                username, titles, rating, message, new Integer(gameNumber)));

        return true;
    }


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processWhisper(String username, String titles, int rating, int gameNumber,
                                     String message) {
        if (titles == null)
            titles = "";

        listenerManager.fireChatEvent(new ChatEvent(this, "whisper", ChatEvent.GAME_CHAT_CATEGORY,
                username, titles, rating, message, new Integer(gameNumber)));

        return true;
    }


    /**
     * Regex for matching tourney tell qtells.
     */

    private static final Pattern TOURNEY_TELL_REGEX =
            Pattern.compile("^(" + USERNAME_REGEX + ")(" + TITLES_REGEX + ")?\\(T(\\d+)\\): (.*)");


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processQTell(String message) {
        ChatEvent evt;
        Matcher matcher = TOURNEY_TELL_REGEX.matcher(message);
        if (matcher.matches()) {
            String sender = matcher.group(1);
            String title = matcher.group(2);
            if (title == null)
                title = "";
            Integer tourneyIndex = new Integer(matcher.group(3));
            message = matcher.group(4);
            evt = new ChatEvent(this, "qtell.tourney", ChatEvent.TOURNEY_CHAT_CATEGORY,
                    sender, title, -1, message, tourneyIndex);
        } else {
            evt = new ChatEvent(this, "qtell", ChatEvent.PERSON_TO_PERSON_CHAT_CATEGORY,
                    null, null, -1, message, null);
        }

        listenerManager.fireChatEvent(evt);

        return true;
    }


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processShout(String username, String titles, String message) {
        listenerManager.fireChatEvent(new ChatEvent(this, "shout", ChatEvent.ROOM_CHAT_CATEGORY,
                username, (titles == null ? "" : titles), -1, message, null));

        return true;
    }


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processIShout(String username, String titles, String message) {
        listenerManager.fireChatEvent(new ChatEvent(this, "ishout", ChatEvent.ROOM_CHAT_CATEGORY,
                username, (titles == null ? "" : titles), -1, message, null));

        return true;
    }


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processTShout(String username, String titles, String message) {
        listenerManager.fireChatEvent(new ChatEvent(this, "tshout", ChatEvent.TOURNEY_CHAT_CATEGORY,
                username, (titles == null ? "" : titles), -1, message, null));

        return true;
    }


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processCShout(String username, String titles, String message) {
        listenerManager.fireChatEvent(new ChatEvent(this, "cshout", ChatEvent.ROOM_CHAT_CATEGORY,
                username, (titles == null ? "" : titles), -1, message, null));

        return true;
    }


    /**
     * Fires an appropriate ChatEvent.
     */

    protected boolean processAnnouncement(String username, String message) {
        listenerManager.fireChatEvent(new ChatEvent(this, "announcement", ChatEvent.BROADCAST_CHAT_CATEGORY,
                username, "", -1, message, null));

        return true;
    }


    /**
     * Returns the wild variant corresponding to the given server wild variant
     * name/category name, or <code>null</code> if that category is not supported.
     */

    private static WildVariant getVariant(String categoryName) {
        if (categoryName.equalsIgnoreCase("lightning") ||
                categoryName.equalsIgnoreCase("blitz") ||
                categoryName.equalsIgnoreCase("standard") ||
                categoryName.equalsIgnoreCase("untimed"))
            return Chess.getInstance();


        if (categoryName.startsWith("wild/")) {
            String wildId = categoryName.substring("wild/".length());
            if (wildId.equals("0") || wildId.equals("1"))
                return new BothSidesCastlingVariant(Chess.INITIAL_POSITION_FEN, categoryName);
            else if (wildId.equals("2") || wildId.equals("3"))
                return new NoCastlingVariant(Chess.INITIAL_POSITION_FEN, categoryName);
            else if (wildId.equals("5") || wildId.equals("8") || wildId.equals("8a"))
                return new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, categoryName);
            else if (wildId.equals("fr"))
                return FischerRandom.getInstance();
        } else if (categoryName.equals("suicide"))
            return Suicide.getInstance();
        else if (categoryName.equals("losers"))
            return new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, categoryName);
        else if (categoryName.equals("atomic"))
            return Atomic.getInstance();

            // This means it's a fake variant we're using because the server hasn't told us the real one.
        else if (categoryName.equals("Unknown variant"))
            return Chess.getInstance();

        return null;
    }


    /**
     * Returns the wild variant name corresponding to the specified wild variant,
     * that can be used for issuing a seek, e.g. "w1" or "fr".
     * Returns null if the specified wild variant is not supported by FICS.
     */

    private String getWildName(WildVariant variant) {
        if (variant == null)
            throw new IllegalArgumentException("Null variant");

        String variantName = variant.getName();
        if (variantName.startsWith("wild/"))
            return "w" + variantName.substring("wild/".length());
        else if (variant.equals(Chess.getInstance()))
            return "";
        else if (variant.equals(FischerRandom.getInstance()))
            return "fr";
        else if (variant.equals(Suicide.getInstance()))
            return "suicide";
        else if (variant.equals(Atomic.getInstance()))
            return "atomic";
        else if ("losers".equals(variantName))
            return "losers";

        return null;
    }


    /**
     * A list of supported wild variants, initialized lazily.
     */

    private static WildVariant[] wildVariants;


    /**
     * Returns a list of support wild variants.
     */

    public WildVariant[] getSupportedVariants() {
        if (wildVariants == null) {
            wildVariants = new WildVariant[]{
                    Chess.getInstance(),
                    FischerRandom.getInstance(),
                    Suicide.getInstance(),
                    Atomic.getInstance(),
                    new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, "losers"),
                    new BothSidesCastlingVariant(Chess.INITIAL_POSITION_FEN, "wild/0"),
                    new BothSidesCastlingVariant(Chess.INITIAL_POSITION_FEN, "wild/1"),
                    new NoCastlingVariant(Chess.INITIAL_POSITION_FEN, "wild/2"),
                    new NoCastlingVariant(Chess.INITIAL_POSITION_FEN, "wild/3"),
                    new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, "wild/5"),
                    new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, "wild/8"),
                    new ChesslikeGenericVariant(Chess.INITIAL_POSITION_FEN, "wild/8a"),
            };
        }

        return (WildVariant[]) wildVariants.clone();
    }


    /**
     * A hashtable where we keep game numbers mapped to GameInfoStruct objects
     * of games that haven't started yet.
     */

    private final Hashtable unstartedGamesData = new Hashtable(1);


    /**
     * Maps game numbers to InternalGameData objects of ongoing games.
     */

    private final Hashtable ongoingGamesData = new Hashtable(5);


    /**
     * A Hashtable mapping Game objects to Vectors of moves which were sent for
     * these games but the server didn't tell us yet whether the move is legal
     * or not.
     */

    private final Hashtable unechoedMoves = new Hashtable(1);


    /**
     * A list of game numbers of ongoing games which we can't support for some
     * reason (not a supported variant for example).
     */

    private final Vector unsupportedGames = new Vector();


    /**
     * The user's primary played (by the user) game, -1 if unknown. This is only
     * set when the user is playing more than one game.
     */

    private int primaryPlayedGame = -1;


    /**
     * The user's primary observed game, -1 if unknown. This is only set when
     * the user is observing more than one game.
     */

    private int primaryObservedGame = -1;


    /**
     * Returns the game with the specified number.
     * This method (currently) exists solely for the benefit of the arrow/circle
     * script.
     */

    public Game getGame(int gameNumber) throws NoSuchGameException {
        return getGameData(gameNumber).game;
    }


    /**
     * Returns the InternalGameData for the ongoing game with the specified
     * number. Throws a <code>NoSuchGameException</code> if there's no such game.
     */

    private InternalGameData getGameData(int gameNumber) throws NoSuchGameException {
        InternalGameData gameData = (InternalGameData) ongoingGamesData.get(new Integer(gameNumber));
        if (gameData == null)
            throw new NoSuchGameException();

        return gameData;
    }


    /**
     * Finds the (primary) game played by the user. Throws a
     * <code>NoSuchGameException</code> if there's no such game.
     */

    private InternalGameData findMyGame() throws NoSuchGameException {
        if (primaryPlayedGame != -1)
            return getGameData(primaryPlayedGame);

        Enumeration gameNumbers = ongoingGamesData.keys();
        while (gameNumbers.hasMoreElements()) {
            Integer gameNumber = (Integer) gameNumbers.nextElement();
            InternalGameData gameData = (InternalGameData) ongoingGamesData.get(gameNumber);
            Game game = gameData.game;
            if (game.getGameType() == Game.MY_GAME)
                return gameData;
        }

        throw new NoSuchGameException();
    }


    /**
     * Finds the played user's game against the specified opponent.
     * Returns the game number of null if no such game exists.
     */

    private InternalGameData findMyGameAgainst(String playerName) throws NoSuchGameException {
        Enumeration gameNumbers = ongoingGamesData.keys();
        while (gameNumbers.hasMoreElements()) {
            Integer gameNumber = (Integer) gameNumbers.nextElement();
            InternalGameData gameData = (InternalGameData) ongoingGamesData.get(gameNumber);
            Game game = gameData.game;
            Player userPlayer = game.getUserPlayer();
            if (userPlayer == null) // Not our game or not played
                continue;
            Player oppPlayer = userPlayer.getOpponent();
            if ((oppPlayer.isWhite() && game.getWhiteName().equals(playerName)) ||
                    (oppPlayer.isBlack() && game.getBlackName().equals(playerName)))
                return gameData;
        }

        throw new NoSuchGameException();
    }


    /**
     * Saves the GameInfoStruct until we receive enough info to fire a
     * GameStartEvent.
     */

    protected boolean processGameInfo(GameInfoStruct data) {
        unstartedGamesData.put(new Integer(data.getGameNumber()), data);

        return true;
    }


    /**
     * Fires an appropriate GameEvent depending on the situation.
     */

    protected boolean processStyle12(Style12Struct boardData) {
        Integer gameNumber = new Integer(boardData.getGameNumber());
        InternalGameData gameData = (InternalGameData) ongoingGamesData.get(gameNumber);
        GameInfoStruct unstartedGameInfo = (GameInfoStruct) unstartedGamesData.remove(gameNumber);

        if (unstartedGameInfo != null) // A new game
            gameData = startGame(unstartedGameInfo, boardData);
        else if (gameData != null) { // A known game
            Style12Struct oldBoardData = gameData.boardData;
            int plyDifference = boardData.getPlayedPlyCount() - oldBoardData.getPlayedPlyCount();

            if (plyDifference < 0)
                tryIssueTakeback(gameData, boardData);
            else if (plyDifference == 0) {
                if (!oldBoardData.getBoardFEN().equals(boardData.getBoardFEN()))
                    changePosition(gameData, boardData);

                // This happens if you:
                // 1. Issue "refresh".
                // 2. Make an illegal move, because the server will re-send us the board
                //    (although we don't need it)
                // 3. Issue board setup commands.
                // 4. Use "wname" or "bname" to change the names of the white or black
                //    players.
            } else if (plyDifference == 1) {
                if (boardData.getMoveVerbose() != null)
                    makeMove(gameData, boardData);
                else
                    changePosition(gameData, boardData);
                // This shouldn't happen, but I'll leave it just in case
            } else if (plyDifference > 1) {
                changePosition(gameData, boardData);
                // This happens if you:
                // 1. Issue "forward" with an argument of 2 or bigger.
            }
        } else if (!unsupportedGames.contains(gameNumber)) {
            // Grr, the server started a game without sending us a GameInfo line.
            // Currently happens if you start examining a game (26.08.2002), or
            // doing "refresh <game>" (04.07.2004).

            // We have no choice but to fake the data, since the server simply doesn't
            // send us this information.
            GameInfoStruct fakeGameInfo = new GameInfoStruct(boardData.getGameNumber(),
                    false, "Unknown variant", false, false, false, boardData.getInitialTime(),
                    boardData.getIncrement(), boardData.getInitialTime(), boardData.getIncrement(),
                    0, -1, ' ', -1, ' ', false, false);

            gameData = startGame(fakeGameInfo, boardData);
        }

        if (gameData != null)
            updateGame(gameData, boardData);

        return true;
    }


    /**
     * Processes a delta-board. Instead of actually handing the delta-board, this
     * method, instead, creates a Style12Struct object and then asks
     * <code>processStyle12</code> to handle it.
     */

    protected boolean processDeltaBoard(DeltaBoardStruct data) {
        Integer gameNumber = new Integer(data.getGameNumber());
        InternalGameData gameData = (InternalGameData) ongoingGamesData.get(gameNumber);

        Game game = gameData.game;
        if (game.getVariant() != Chess.getInstance())
            throw new IllegalStateException("delta-boards should only be sent for regular chess");

        Style12Struct lastBoardData = gameData.boardData;
        Vector moveList = gameData.moveList;

        Position pos = game.getInitialPosition();
        for (int i = 0; i < moveList.size(); i++)
            pos.makeMove((Move) moveList.elementAt(i));

        ChessMove move = (ChessMove) (Move.parseWarrenSmith(data.getMoveSmith(), pos, data.getMoveAlgebraic()));

        Square startSquare = move.getStartingSquare();
        ChessPiece movingPiece = (ChessPiece) ((startSquare == null) ? null : pos.getPieceAt(startSquare));

        pos.makeMove(move);


        String boardLexigraphic = pos.getLexigraphic();
        String currentPlayer = pos.getCurrentPlayer().isWhite() ? "W" : "B";
        int doublePawnPushFile = move.getDoublePawnPushFile();
        boolean kingMoved = movingPiece.isKing();
        boolean canWhiteCastleKingside =
                lastBoardData.canWhiteCastleKingside() && !kingMoved && !Square.getInstance(7, 0).equals(startSquare);
        boolean canWhiteCastleQueenside =
                lastBoardData.canBlackCastleQueenside() && !kingMoved && !Square.getInstance(0, 0).equals(startSquare);
        boolean canBlackCastleKingside =
                lastBoardData.canBlackCastleKingside() && !kingMoved && !Square.getInstance(7, 7).equals(startSquare);
        boolean canBlackCastleQueenside =
                lastBoardData.canBlackCastleQueenside() && !kingMoved && !Square.getInstance(0, 7).equals(startSquare);

        boolean isIrreversibleMove = movingPiece.isPawn() || move.isCapture() ||
                (canWhiteCastleKingside != lastBoardData.canWhiteCastleKingside()) ||
                (canWhiteCastleQueenside != lastBoardData.canWhiteCastleQueenside()) ||
                (canBlackCastleKingside != lastBoardData.canBlackCastleKingside()) ||
                (canBlackCastleQueenside != lastBoardData.canBlackCastleQueenside());
        int pliesSinceIrreversible = isIrreversibleMove ? 0 : lastBoardData.getPliesSinceIrreversible() + 1;

        String whiteName = lastBoardData.getWhiteName();
        String blackName = lastBoardData.getBlackName();
        int gameType = lastBoardData.getGameType();
        boolean isPlayedGame = lastBoardData.isPlayedGame();
        boolean isMyTurn = pos.getCurrentPlayer() == game.getUserPlayer();
        int initTime = lastBoardData.getInitialTime();
        int inc = lastBoardData.getIncrement();
        int whiteStrength = calcStrength(pos, Player.WHITE_PLAYER);
        int blackStrength = calcStrength(pos, Player.BLACK_PLAYER);
        int whiteTime = pos.getCurrentPlayer().isBlack() ? data.getRemainingTime() : lastBoardData.getWhiteTime();
        int blackTime = pos.getCurrentPlayer().isWhite() ? data.getRemainingTime() : lastBoardData.getBlackTime();
        int nextMoveNumber = lastBoardData.getNextMoveNumber() + (pos.getCurrentPlayer().isWhite() ? 1 : 0);
        String moveVerbose = createVerboseMove(pos, move);
        String moveSAN = data.getMoveAlgebraic();
        int moveTime = data.getTakenTime();
        boolean isBoardFlipped = lastBoardData.isBoardFlipped();
        boolean isClockRunning = true;
        int lag = 0; // The server doesn't currently send us this information

        Style12Struct boardData = new Style12Struct(boardLexigraphic, currentPlayer, doublePawnPushFile,
                canWhiteCastleKingside, canWhiteCastleQueenside, canBlackCastleKingside, canBlackCastleQueenside,
                pliesSinceIrreversible, gameNumber.intValue(), whiteName, blackName, gameType, isPlayedGame,
                isMyTurn, initTime, inc, whiteStrength, blackStrength, whiteTime, blackTime, nextMoveNumber,
                moveVerbose, moveSAN, moveTime, isBoardFlipped, isClockRunning, lag);

        processStyle12(boardData);

        return true;
    }


    /**
     * Calculates the material strength of the specified player in the specified
     * position.
     */

    private static int calcStrength(Position pos, Player player) {
        int count = 0;
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 8; j++) {
                ChessPiece piece = (ChessPiece) (pos.getPieceAt(i, j));
                if ((piece != null) && (piece.getPlayer() == player)) {
                    if (piece.isPawn())
                        count += 1;
                    else if (piece.isBishop())
                        count += 3;
                    else if (piece.isKnight())
                        count += 3;
                    else if (piece.isRook())
                        count += 5;
                    else if (piece.isQueen())
                        count += 9;
                    else if (piece.isKing())
                        count += 0;
                }
            }
        }

        return count;
    }


    /**
     * Creates a verbose representation of the specified move in the specified
     * position. The move has already been made in the position.
     */

    private static String createVerboseMove(Position pos, ChessMove move) {
        if (move.isShortCastling())
            return "o-o";
        else if (move.isLongCastling())
            return "o-o-o";
        else {
            ChessPiece piece = (ChessPiece) pos.getPieceAt(move.getEndingSquare());
            String moveVerbose = piece.toShortString() + "/" + move.getStartingSquare() + "-" + move.getEndingSquare();
            if (move.isPromotion())
                return moveVerbose + "=" + move.getPromotionTarget().toShortString();
            else
                return moveVerbose;
        }
    }


    /**
     * Changes the bsetup state of the game.
     */

    protected boolean processBSetupMode(boolean entered) {
        try {
            findMyGame().isBSetup = entered;
        } catch (NoSuchGameException e) {
        }

        return super.processBSetupMode(entered);
    }


    /**
     * A small class for keeping internal data about a game.
     */

    private static class InternalGameData {


        /**
         * The Game object representing the game.
         */

        public final Game game;


        /**
         * A list of Moves done in the game.
         */

        public Vector moveList = new Vector();


        /**
         * The last Style12Struct we got for this game.
         */

        public Style12Struct boardData = null;


        /**
         * Is this game in bsetup mode?
         */

        public boolean isBSetup = false;


        /**
         * Maps offer indices to offers. Offers are Pairs where the first element
         * is the <code>Player</code> who made the offer and the 2nd is the offer
         * id. Takeback offers are kept separately.
         */

        public final Hashtable indicesToOffers = new Hashtable();


        /**
         * Maps takeback offer indices to takeback offers. Takeback offers are Pairs
         * where the first element is the <code>Player</code> who made the offer
         * and the 2nd is an <code>Integer</code> specifying the amount of plies
         * offered to take back.
         */

        public final Hashtable indicesToTakebackOffers = new Hashtable();


        /**
         * Works as a set of the offers currently in this game. The elements are
         * Pairs in which the first item is the <code>Player</code> who made the
         * offer and the second one is the offer id. Takeback offers are kept
         * separately.
         */

        private final Hashtable offers = new Hashtable();


        /**
         * The number of plies the white player offerred to takeback.
         */

        private int whiteTakeback;


        /**
         * The number of plies the black player offerred to takeback.
         */

        private int blackTakeback;


        /**
         * Creates a new InternalGameData.
         */

        public InternalGameData(Game game) {
            this.game = game;
        }


        /**
         * Returns the amount of moves made in the game (as far as we counted).
         */

        public int getMoveCount() {
            return moveList.size();
        }


        /**
         * Adds the specified move to the moves list.
         */

        public void addMove(Move move) {
            moveList.addElement(move);
        }


        /**
         * Removes the last <code>count</code> moves from the movelist, if possible.
         * Otherwise, throws an <code>IllegalArgumentException</code>.
         */

        public void removeLastMoves(int count) {
            if (count > moveList.size())
                throw new IllegalArgumentException("Can't remove more elements than there are elements");

            int first = moveList.size() - 1;
            int last = moveList.size() - count;
            for (int i = first; i >= last; i--)
                moveList.removeElementAt(i);
        }


        /**
         * Removes all the moves made in the game.
         */

        public void clearMoves() {
            moveList.removeAllElements();
        }


        /**
         * Returns true if the specified offer is currently made by the specified
         * player in this game.
         */

        public boolean isOffered(int offerId, Player player) {
            return offers.containsKey(new Pair(player, new Integer(offerId)));
        }


        /**
         * Sets the state of the specified offer in the game. Takeback offers are
         * handled by the setTakebackCount method.
         */

        public void setOffer(int offerId, Player player, boolean isMade) {
            Pair offer = new Pair(player, new Integer(offerId));
            if (isMade)
                offers.put(offer, offer);
            else
                offers.remove(offer);
        }


        /**
         * Sets the takeback offer in the game to the specified amount of plies.
         */

        public void setTakebackOffer(Player player, int plies) {
            if (player.isWhite())
                whiteTakeback = plies;
            else
                blackTakeback = plies;
        }


        /**
         * Returns the amount of plies offered to take back by the specified player.
         */

        public int getTakebackOffer(Player player) {
            if (player.isWhite())
                return whiteTakeback;
            else
                return blackTakeback;
        }


    }


    /**
     * Changes the primary played game.
     */

    protected boolean processSimulCurrentBoardChanged(int gameNumber, String oppName) {
        primaryPlayedGame = gameNumber;

        return true;
    }


    /**
     * Changes the primary observed game.
     */

    protected boolean processPrimaryGameChanged(int gameNumber) {
        primaryObservedGame = gameNumber;

        return true;
    }


    /**
     * Invokes <code>closeGame(int)</code>.
     */

    protected boolean processGameEnd(int gameNumber, String whiteName, String blackName,
                                     String reason, String result) {

        int resultCode;
        if ("1-0".equals(result))
            resultCode = Game.WHITE_WINS;
        else if ("0-1".equals(result))
            resultCode = Game.BLACK_WINS;
        else if ("1/2-1/2".equals(result))
            resultCode = Game.DRAW;
        else
            resultCode = Game.UNKNOWN_RESULT;

        closeGame(gameNumber, resultCode);

        return false;
    }


    /**
     * Invokes <code>closeGame(int)</code>.
     */

    protected boolean processStoppedObserving(int gameNumber) {
        closeGame(gameNumber, Game.UNKNOWN_RESULT);

        return false;
    }


    /**
     * Invokes <code>closeGame(int)</code>.
     */

    protected boolean processStoppedExamining(int gameNumber) {
        closeGame(gameNumber, Game.UNKNOWN_RESULT);

        return false;
    }


    /**
     * Invokes <code>illegalMoveAttempted</code>.
     */

    protected boolean processIllegalMove(String moveString, String reason) {
        illegalMoveAttempted(moveString);

        return false;
    }


    /**
     * This method informs the user that he tried to use (observe, play etc.)
     * a wild variant not supported by Jin. Please use this method when
     * appropriate instead of sending your own message.
     */

    protected void warnVariantUnsupported(String variantName) {
        Object[] messageFormatArgs = new Object[]{variantName};
        String message =
                I18n.get(JinFreechessConnection.class).getFormattedString("unsupportedVariantMessage", messageFormatArgs);
        String[] messageLines = message.split("\n");

        int maxLineLength = 0;
        for (int i = 0; i < messageLines.length; i++)
            if (messageLines[i].length() > maxLineLength)
                maxLineLength = messageLines[i].length();

        String border = TextUtilities.padStart("", '*', maxLineLength + 4);

        processLine(border);
        for (int i = 0; i < messageLines.length; i++)
            processLine("* " + TextUtilities.padEnd(messageLines[i], ' ', maxLineLength) + " *");
        processLine(border);
    }


    /**
     * Called when a new game is starting. Responsible for creating the game on
     * the client side and firing appropriate events. Returns an InternalGameData
     * instance for the newly created Game.
     */

    private InternalGameData startGame(GameInfoStruct gameInfo, Style12Struct boardData) {
        String categoryName = gameInfo.getGameCategory();
        WildVariant variant = getVariant(categoryName);
        if (variant == null) {
            warnVariantUnsupported(categoryName);
            unsupportedGames.addElement(new Integer(gameInfo.getGameNumber()));
            return null;
        }

        int gameType;
        switch (boardData.getGameType()) {
            case Style12Struct.MY_GAME:
                gameType = Game.MY_GAME;
                break;
            case Style12Struct.OBSERVED_GAME:
                gameType = Game.OBSERVED_GAME;
                break;
            case Style12Struct.ISOLATED_BOARD:
                gameType = Game.ISOLATED_BOARD;
                break;
            default:
                throw new IllegalArgumentException("Bad game type value: " + boardData.getGameType());
        }

        Position initPos = new Position(variant);
        initPos.setFEN(boardData.getBoardFEN());

        String whiteName = boardData.getWhiteName();
        String blackName = boardData.getBlackName();

        int whiteTime = 1000 * gameInfo.getWhiteTime();
        int blackTime = 1000 * gameInfo.getBlackTime();
        int whiteInc = 1000 * gameInfo.getWhiteInc();
        int blackInc = 1000 * gameInfo.getBlackInc();

        int whiteRating = gameInfo.isWhiteRegistered() ? -1 : gameInfo.getWhiteRating();
        int blackRating = gameInfo.isBlackRegistered() ? -1 : gameInfo.getBlackRating();

        String gameID = String.valueOf(gameInfo.getGameNumber());

        boolean isRated = gameInfo.isGameRated();

        boolean isPlayed = boardData.isPlayedGame();

        String whiteTitles = "";
        String blackTitles = "";

        boolean initiallyFlipped = boardData.isBoardFlipped();

        Player currentPlayer = playerForString(boardData.getCurrentPlayer());
        Player userPlayer = null;
        if ((gameType == Game.MY_GAME) && isPlayed)
            userPlayer = boardData.isMyTurn() ? currentPlayer : currentPlayer.getOpponent();

        Game game = new Game(gameType, initPos, boardData.getPlayedPlyCount(), whiteName, blackName,
                whiteTime, whiteInc, blackTime, blackInc, whiteRating, blackRating, gameID, categoryName,
                isRated, isPlayed, whiteTitles, blackTitles, initiallyFlipped, userPlayer);

        InternalGameData gameData = new InternalGameData(game);

        ongoingGamesData.put(new Integer(gameInfo.getGameNumber()), gameData);

        listenerManager.fireGameEvent(new GameStartEvent(this, game));

        // The server doesn't send us seek remove lines during games, so we have
        // no choice but to remove *all* seeks during a game. The seeks are restored
        // when a game ends by setting seekinfo to 1 again.
        if (gameType == Game.MY_GAME)
            removeAllSeeks();

        return gameData;
    }


    /**
     * Updates any game parameters that differ in the board data from the current
     * game data.
     */

    private void updateGame(InternalGameData gameData, Style12Struct boardData) {
        Game game = gameData.game;
        Style12Struct oldBoardData = gameData.boardData;

        updateClocks(gameData, boardData); // Update the clocks

        // Flip board
        if ((oldBoardData != null) && (oldBoardData.isBoardFlipped() != boardData.isBoardFlipped()))
            flipBoard(gameData, boardData);

        game.setWhiteName(boardData.getWhiteName()); // Change white name
        game.setBlackName(boardData.getBlackName()); // Change black name
        game.setWhiteTime(1000 * boardData.getInitialTime()); // Change white's initial time
        game.setWhiteInc(1000 * boardData.getIncrement()); // Change white's increment
        game.setBlackTime(1000 * boardData.getInitialTime()); // Change black's initial time
        game.setBlackInc(1000 * boardData.getIncrement()); // Change black's increment


        gameData.boardData = boardData;
    }


    /**
     * Gets called when a move is made. Fires an appropriate MoveMadeEvent.
     */

    private void makeMove(InternalGameData gameData, Style12Struct boardData) {
        Game game = gameData.game;
        Style12Struct oldBoardData = gameData.boardData;

        String moveVerbose = boardData.getMoveVerbose();
        String moveSAN = boardData.getMoveSAN();

        WildVariant variant = game.getVariant();
        Position position = new Position(variant);
        position.setLexigraphic(oldBoardData.getBoardLexigraphic());
        Player currentPlayer = playerForString(oldBoardData.getCurrentPlayer());
        position.setCurrentPlayer(currentPlayer);

        Move move;
        Square fromSquare, toSquare;
        Piece promotionPiece = null;
        if (moveVerbose.equals("o-o"))
            move = variant.createShortCastling(position);
        else if (moveVerbose.equals("o-o-o"))
            move = variant.createLongCastling(position);
        else {
            fromSquare = Square.parseSquare(moveVerbose.substring(2, 4));
            toSquare = Square.parseSquare(moveVerbose.substring(5, 7));
            int promotionCharIndex = moveVerbose.indexOf("=") + 1;
            if (promotionCharIndex != 0) {
                String pieceString = moveVerbose.substring(promotionCharIndex, promotionCharIndex + 1);
                if (currentPlayer.isBlack()) // The server always sends upper case characters, even for black pieces.
                    pieceString = pieceString.toLowerCase();
                promotionPiece = variant.parsePiece(pieceString);
            }

            move = variant.createMove(position, fromSquare, toSquare, promotionPiece, moveSAN);
        }

        listenerManager.fireGameEvent(new MoveMadeEvent(this, game, move, true));
        // (isNew == true) because FICS never sends the entire move history

        Vector unechoedGameMoves = (Vector) unechoedMoves.get(game);
        if ((unechoedGameMoves != null) && (unechoedGameMoves.size() != 0)) { // Might be our move.
            Move madeMove = (Move) unechoedGameMoves.elementAt(0);
            if (isSameMove(game, move, madeMove))
                unechoedGameMoves.removeElementAt(0);
        }

        gameData.addMove(move);
    }


    /**
     * Returns whether <code>echoedMove</code> (sent to us by the server)
     * is the same move as <code>sentMove</code> (a move we sent to the server).
     */

    private static boolean isSameMove(Game game, Move echoedMove, Move sentMove) {
        try {
            String echoedMoveString = moveToString(game, echoedMove);
            String sentMoveString = moveToString(game, sentMove);
            return echoedMoveString.equals(sentMoveString);
        } catch (IllegalArgumentException e) {
            // An exception shouldn't be thrown for sentMove (since moveToString was
            // already called on it when it was sent to the server). Thus if it is
            // thrown, it's for echoedMove, in which case it's certainly not the
            // same move.
            return false;
        }
    }


    /**
     * Fires an appropriate ClockAdjustmentEvent.
     */

    private void updateClocks(InternalGameData gameData, Style12Struct boardData) {
        Game game = gameData.game;

        int whiteTime = boardData.getWhiteTime();
        int blackTime = boardData.getBlackTime();

        Player currentPlayer = playerForString(boardData.getCurrentPlayer());

        // Don't make clocks run for an isolated position.
        boolean isIsolatedBoard = game.getGameType() == Game.ISOLATED_BOARD;
        boolean whiteRunning = (!isIsolatedBoard) && boardData.isClockRunning() && currentPlayer.isWhite();
        boolean blackRunning = (!isIsolatedBoard) && boardData.isClockRunning() && currentPlayer.isBlack();

        listenerManager.fireGameEvent(new ClockAdjustmentEvent(this, game, Player.WHITE_PLAYER, whiteTime, whiteRunning));
        listenerManager.fireGameEvent(new ClockAdjustmentEvent(this, game, Player.BLACK_PLAYER, blackTime, blackRunning));
    }


    /**
     * Fires an appropriate GameEndEvent.
     */

    private void closeGame(int gameNumber, int result) {
        Integer gameID = new Integer(gameNumber);

        if (gameID.intValue() == primaryPlayedGame)
            primaryPlayedGame = -1;
        else if (gameID.intValue() == primaryObservedGame)
            primaryObservedGame = -1;

        InternalGameData gameData = (InternalGameData) ongoingGamesData.remove(gameID);
        if (gameData != null) {
            Game game = gameData.game;

            game.setResult(result);
            listenerManager.fireGameEvent(new GameEndEvent(this, game, result));

            if ((game.getGameType() == Game.MY_GAME) && getIvarState(Ivar.SEEKINFO))
                setIvarState(Ivar.SEEKINFO, true); // Refresh the seeks
        } else
            unsupportedGames.removeElement(gameID);
    }


    /**
     * Fires an appropriate BoardFlipEvent.
     */

    private void flipBoard(InternalGameData gameData, Style12Struct newBoardData) {
        listenerManager.fireGameEvent(new BoardFlipEvent(this, gameData.game, newBoardData.isBoardFlipped()));
    }


    /**
     * Fires an appropriate IllegalMoveEvent.
     */

    private void illegalMoveAttempted(String moveString) {
        try {
            InternalGameData gameData = findMyGame();
            Game game = gameData.game;

            Vector unechoedGameMoves = (Vector) unechoedMoves.get(game);

            // Not a move we made (probably the user typed it in)
            if ((unechoedGameMoves == null) || (unechoedGameMoves.size() == 0))
                return;


            Move move = (Move) unechoedGameMoves.elementAt(0);

            // We have no choice but to allow (moveString == null) because the server
            // doesn't always send us the move string (for example if it's not our turn).
            if ((moveString == null) || moveToString(game, move).equals(moveString)) {
                // Our move, probably

                unechoedGameMoves.removeAllElements();
                listenerManager.fireGameEvent(new IllegalMoveEvent(this, game, move));
            }
        } catch (NoSuchGameException e) {
        }
    }


    /**
     * Determines whether it's possible to issue a takeback for the specified
     * game change and if so calls issueTakeback, otherwise calls changePosition.
     */

    private void tryIssueTakeback(InternalGameData gameData, Style12Struct boardData) {
        Style12Struct oldBoardData = gameData.boardData;
        int plyDifference = oldBoardData.getPlayedPlyCount() - boardData.getPlayedPlyCount();

        if ((gameData.getMoveCount() < plyDifference)) // Can't issue takeback
            changePosition(gameData, boardData);
        else if (gameData.isBSetup)
            changePosition(gameData, boardData);
        else {
            Game game = gameData.game;
            Vector moveList = gameData.moveList;
            // Check whether the positions match, otherwise it could just be someone
            // issuing "bsetup fen ..." after making a few moves which resets the ply
            // count.

            Position oldPos = game.getInitialPosition();
            for (int i = 0; i < moveList.size() - plyDifference; i++) {
                Move move = (Move) moveList.elementAt(i);
                oldPos.makeMove(move);
            }

            Position newPos = game.getInitialPosition();
            newPos.setFEN(boardData.getBoardFEN());

            if (newPos.equals(oldPos))
                issueTakeback(gameData, boardData);
            else
                changePosition(gameData, boardData);
        }
    }


    /**
     * Fires an appropriate TakebackEvent.
     */

    private void issueTakeback(InternalGameData gameData, Style12Struct newBoardData) {
        Style12Struct oldBoardData = gameData.boardData;
        int takebackCount = oldBoardData.getPlayedPlyCount() - newBoardData.getPlayedPlyCount();

        listenerManager.fireGameEvent(new TakebackEvent(this, gameData.game, takebackCount));

        gameData.removeLastMoves(takebackCount);
    }


    /**
     * Fires an appropriate PositionChangedEvent.
     */

    private void changePosition(InternalGameData gameData, Style12Struct newBoardData) {
        Game game = gameData.game;

        Position newPos = game.getInitialPosition();
        newPos.setFEN(newBoardData.getBoardFEN());

        game.setInitialPosition(newPos);
        game.setPliesSinceStart(newBoardData.getPlayedPlyCount());

        listenerManager.fireGameEvent(new PositionChangedEvent(this, game, newPos));

        gameData.clearMoves();

        // We do this because moves in bsetup mode cause position change events, not move events
        if (gameData.isBSetup) {
            Vector unechoedGameMoves = (Vector) unechoedMoves.get(game);
            if ((unechoedGameMoves != null) && (unechoedGameMoves.size() != 0))
                unechoedGameMoves.removeElementAt(0);
        }
    }


    /**
     * Maps seek IDs to Seek objects currently in the sought list.
     */

    private final Hashtable seeks = new Hashtable();


    /**
     * Returns the SeekListenerManager via which you can register and unregister
     * SeekListeners.
     */

    public SeekListenerManager getSeekListenerManager() {
        return getFreechessListenerManager();
    }


    /**
     * Creates an appropriate Seek object and fires a SeekEvent.
     */

    protected boolean processSeekAdded(SeekInfoStruct seekInfo) {
        // We may get seeks after setting seekinfo to false because the server
        // already sent them when we sent it the request to set seekInfo to false.
        if (getRequestedIvarState(Ivar.SEEKINFO)) {
            WildVariant variant = getVariant(seekInfo.getMatchType());
            if (variant != null) {
                String seekID = String.valueOf(seekInfo.getSeekIndex());
                StringBuffer titlesBuf = new StringBuffer();
                int titles = seekInfo.getSeekerTitles();

                if ((titles & SeekInfoStruct.COMPUTER) != 0)
                    titlesBuf.append("(C)");
                if ((titles & SeekInfoStruct.GM) != 0)
                    titlesBuf.append("(GM)");
                if ((titles & SeekInfoStruct.IM) != 0)
                    titlesBuf.append("(IM)");
                if ((titles & SeekInfoStruct.FM) != 0)
                    titlesBuf.append("(FM)");
                if ((titles & SeekInfoStruct.WGM) != 0)
                    titlesBuf.append("(WGM)");
                if ((titles & SeekInfoStruct.WIM) != 0)
                    titlesBuf.append("(WIM)");
                if ((titles & SeekInfoStruct.WFM) != 0)
                    titlesBuf.append("(WFM)");

                boolean isProvisional = (seekInfo.getSeekerProvShow() == 'P');

                boolean isSeekerRated = (seekInfo.getSeekerRating() != 0);

                boolean isRegistered = ((seekInfo.getSeekerTitles() & SeekInfoStruct.UNREGISTERED) == 0);

                boolean isComputer = ((seekInfo.getSeekerTitles() & SeekInfoStruct.COMPUTER) != 0);

                Player color;
                switch (seekInfo.getSeekerColor()) {
                    case 'W':
                        color = Player.WHITE_PLAYER;
                        break;
                    case 'B':
                        color = Player.BLACK_PLAYER;
                        break;
                    case '?':
                        color = null;
                        break;
                    default:
                        throw new IllegalStateException("Bad desired color char: " + seekInfo.getSeekerColor());
                }

                boolean isRatingLimited = ((seekInfo.getOpponentMinRating() > 0) || (seekInfo.getOpponentMaxRating() < 9999));

                Seek seek = new Seek(seekID, seekInfo.getSeekerHandle(), titlesBuf.toString(), seekInfo.getSeekerRating(),
                        isProvisional, isRegistered, isSeekerRated, isComputer, variant, seekInfo.getMatchType(),
                        seekInfo.getMatchTime() * 60 * 1000, seekInfo.getMatchIncrement() * 1000, seekInfo.isMatchRated(), color,
                        isRatingLimited, seekInfo.getOpponentMinRating(), seekInfo.getOpponentMaxRating(),
                        !seekInfo.isAutomaticAccept(), seekInfo.isFormulaUsed());

                Integer seekIndex = new Integer(seekInfo.getSeekIndex());

                Seek oldSeek = (Seek) seeks.get(seekIndex);
                if (oldSeek != null)
                    listenerManager.fireSeekEvent(new SeekEvent(this, SeekEvent.SEEK_REMOVED, oldSeek));

                seeks.put(seekIndex, seek);
                listenerManager.fireSeekEvent(new SeekEvent(this, SeekEvent.SEEK_ADDED, seek));
            }
        }

        return true;
    }


    /**
     * Issues the appropriate SeekEvents and removes the seeks.
     */

    protected boolean processSeeksRemoved(int[] removedSeeks) {
        for (int i = 0; i < removedSeeks.length; i++) {
            Integer seekIndex = new Integer(removedSeeks[i]);
            Seek seek = (Seek) seeks.get(seekIndex);
            if (seek == null) // Happens if the seek is one we didn't fire an event for,
                continue;       // for example if we don't support the variant.

            listenerManager.fireSeekEvent(new SeekEvent(this, SeekEvent.SEEK_REMOVED, seek));

            seeks.remove(seekIndex);
        }

        return true;
    }


    /**
     * Issues the appropriate SeeksEvents and removes the seeks.
     */

    protected boolean processSeeksCleared() {
        removeAllSeeks();
        return true;
    }


    /**
     * Removes all the seeks and notifies the listeners.
     */

    private void removeAllSeeks() {
        int seeksCount = seeks.size();
        if (seeksCount != 0) {
            Object[] seeksIndices = new Object[seeksCount];

            // Copy all the keys into a temporary array
            Enumeration seekIDsEnum = seeks.keys();
            for (int i = 0; i < seeksCount; i++)
                seeksIndices[i] = seekIDsEnum.nextElement();

            // Remove all the seeks one by one, notifying any interested listeners.
            for (int i = 0; i < seeksCount; i++) {
                Object seekIndex = seeksIndices[i];
                Seek seek = (Seek) seeks.get(seekIndex);
                listenerManager.fireSeekEvent(new SeekEvent(this, SeekEvent.SEEK_REMOVED, seek));
                seeks.remove(seekIndex);
            }
        }
    }


    /**
     * This method is called by our FreechessJinListenerManager when a new
     * SeekListener is added and we already had registered listeners (meaning that
     * iv_seekinfo was already on, so we need to notify the new listeners of all
     * existing seeks as well).
     */

    void notFirstListenerAdded(SeekListener listener) {
        Enumeration seeksEnum = seeks.elements();
        while (seeksEnum.hasMoreElements()) {
            Seek seek = (Seek) seeksEnum.nextElement();
            SeekEvent evt = new SeekEvent(this, SeekEvent.SEEK_ADDED, seek);
            listener.seekAdded(evt);
        }
    }


    /**
     * This method is called by our ChessclubJinListenerManager when the last
     * SeekListener is removed.
     */

    void lastSeekListenerRemoved() {
        seeks.clear();
    }


    /**
     * Maps offer indices to the <code>InternalGameData</code> objects
     * representing the games in which the offer was made.
     */

    private final Hashtable offerIndicesToGameData = new Hashtable();


    /**
     * Override processOffer to always return true, since we don't want the
     * user to ever see these messages.
     */


    protected boolean processOffer(boolean toUser, String offerType, int offerIndex,
                                   String oppName, String offerParams) {

        super.processOffer(toUser, offerType, offerIndex, oppName, offerParams);
        return true;
    }


    /**
     * Overrides the superclass' method only to return true.
     */

    protected boolean processMatchOffered(boolean toUser, int offerIndex, String oppName,
                                          String matchDetails) {
        super.processMatchOffered(toUser, offerIndex, oppName, matchDetails);

        return true;
    }


    /**
     * Fires the appropriate OfferEvent(s).
     */

    protected boolean processTakebackOffered(boolean toUser, int offerIndex, String oppName,
                                             int takebackCount) {
        super.processTakebackOffered(toUser, offerIndex, oppName, takebackCount);

        try {
            InternalGameData gameData = findMyGameAgainst(oppName);
            Player userPlayer = gameData.game.getUserPlayer();
            Player player = toUser ? userPlayer.getOpponent() : userPlayer;

            offerIndicesToGameData.put(new Integer(offerIndex), gameData);
            gameData.indicesToTakebackOffers.put(new Integer(offerIndex),
                    new Pair(player, new Integer(takebackCount)));

            updateTakebackOffer(gameData, player, takebackCount);
        } catch (NoSuchGameException e) {
        }

        return true;
    }


    /**
     * Fires the appropriate OfferEvent(s).
     */

    protected boolean processDrawOffered(boolean toUser, int offerIndex, String oppName) {
        super.processDrawOffered(toUser, offerIndex, oppName);

        processOffered(toUser, offerIndex, oppName, OfferEvent.DRAW_OFFER);

        return true;
    }


    /**
     * Fires the appropriate OfferEvent(s).
     */

    protected boolean processAbortOffered(boolean toUser, int offerIndex, String oppName) {
        super.processAbortOffered(toUser, offerIndex, oppName);

        processOffered(toUser, offerIndex, oppName, OfferEvent.ABORT_OFFER);

        return true;
    }


    /**
     * Fires the appropriate OfferEvent(s).
     */

    protected boolean processAdjournOffered(boolean toUser, int offerIndex, String oppName) {
        super.processAdjournOffered(toUser, offerIndex, oppName);

        processOffered(toUser, offerIndex, oppName, OfferEvent.ADJOURN_OFFER);

        return true;
    }


    /**
     * Gets called by the various process[offerType]Offered() methods to handle
     * the offers uniformly.
     */

    private void processOffered(boolean toUser, int offerIndex, String oppName, int offerId) {
        try {
            InternalGameData gameData = findMyGameAgainst(oppName);
            Player userPlayer = gameData.game.getUserPlayer();
            Player player = toUser ? userPlayer.getOpponent() : userPlayer;

            offerIndicesToGameData.put(new Integer(offerIndex), gameData);
            gameData.indicesToOffers.put(new Integer(offerIndex),
                    new Pair(player, new Integer(offerId)));

            updateOffers(gameData, offerId, player, true);
        } catch (NoSuchGameException e) {
        }
    }


    /**
     * Fires the appropriate OfferEvent(s).
     */

    protected boolean processOfferRemoved(int offerIndex) {
        super.processOfferRemoved(offerIndex);

        InternalGameData gameData =
                (InternalGameData) offerIndicesToGameData.remove(new Integer(offerIndex));

        if (gameData != null) {
            // Check regular offers
            Pair offer = (Pair) gameData.indicesToOffers.remove(new Integer(offerIndex));
            if (offer != null) {
                Player player = (Player) offer.getFirst();
                int offerId = ((Integer) offer.getSecond()).intValue();
                updateOffers(gameData, offerId, player, false);
            } else {
                // Check takeback offers
                offer = (Pair) gameData.indicesToTakebackOffers.remove(new Integer(offerIndex));
                if (offer != null) {
                    Player player = (Player) offer.getFirst();
                    updateTakebackOffer(gameData, player, 0);
                }
            }
        }

        return true;
    }


    /**
     * Fires the appropriate OfferEvent(s).
     */

    protected boolean processPlayerCounteredTakebackOffer(int gameNum, String playerName,
                                                          int takebackCount) {
        super.processPlayerCounteredTakebackOffer(gameNum, playerName, takebackCount);

        try {
            InternalGameData gameData = getGameData(gameNum);
            Player player = gameData.game.getPlayerNamed(playerName);

            updateTakebackOffer(gameData, player.getOpponent(), 0);
            updateTakebackOffer(gameData, player, takebackCount);
        } catch (NoSuchGameException e) {
        }

        return false;
    }


    /**
     * Fires the appropriate OfferEvent(s).
     */

    protected boolean processPlayerOffered(int gameNum, String playerName, String offerName) {
        super.processPlayerOffered(gameNum, playerName, offerName);

        try {
            InternalGameData gameData = getGameData(gameNum);
            Player player = gameData.game.getPlayerNamed(playerName);
            int offerId;
            try {
                offerId = offerIdForOfferName(offerName);
                updateOffers(gameData, offerId, player, true);
            } catch (IllegalArgumentException e) {
            }
        } catch (NoSuchGameException e) {
        }

        return false;
    }


    /**
     * Fires the appropriate OfferEvent(s).
     */

    protected boolean processPlayerDeclined(int gameNum, String playerName, String offerName) {
        super.processPlayerDeclined(gameNum, playerName, offerName);

        try {
            InternalGameData gameData = getGameData(gameNum);
            Player player = gameData.game.getPlayerNamed(playerName);
            int offerId;
            try {
                offerId = offerIdForOfferName(offerName);
                updateOffers(gameData, offerId, player.getOpponent(), false);
            } catch (IllegalArgumentException e) {
            }
        } catch (NoSuchGameException e) {
        }

        return false;
    }


    /**
     * Fires the appropriate OfferEvent(s).
     */

    protected boolean processPlayerWithdrew(int gameNum, String playerName, String offerName) {
        super.processPlayerWithdrew(gameNum, playerName, offerName);

        try {
            InternalGameData gameData = getGameData(gameNum);
            Player player = gameData.game.getPlayerNamed(playerName);
            int offerId;
            try {
                offerId = offerIdForOfferName(offerName);
                updateOffers(gameData, offerId, player, false);
            } catch (IllegalArgumentException e) {
            }
        } catch (NoSuchGameException e) {
        }

        return false;
    }


    /**
     * Fires the appropriate OfferEvent(s).
     */

    protected boolean processPlayerOfferedTakeback(int gameNum, String playerName, int takebackCount) {
        super.processPlayerOfferedTakeback(gameNum, playerName, takebackCount);

        try {
            InternalGameData gameData = getGameData(gameNum);
            Player player = gameData.game.getPlayerNamed(playerName);

            updateTakebackOffer(gameData, player, takebackCount);
        } catch (NoSuchGameException e) {
        }

        return false;
    }


    /**
     * Returns the offerId (as defined by OfferEvent) corresponding to the
     * specified offer name. Throws an IllegalArgumentException if the offer name
     * is not recognizes.
     */

    private static int offerIdForOfferName(String offerName) throws IllegalArgumentException {
        if ("draw".equals(offerName))
            return OfferEvent.DRAW_OFFER;
        else if ("abort".equals(offerName))
            return OfferEvent.ABORT_OFFER;
        else if ("adjourn".equals(offerName))
            return OfferEvent.ADJOURN_OFFER;
        else if ("takeback".equals(offerName))
            return OfferEvent.TAKEBACK_OFFER;
        else
            throw new IllegalArgumentException("Unknown offer name: " + offerName);
    }


    /**
     * Updates the specified offer, firing any necessary events.
     */

    private void updateOffers(InternalGameData gameData, int offerId, Player player, boolean on) {
        Game game = gameData.game;

        if (offerId == OfferEvent.TAKEBACK_OFFER) {
            // We're forced to fake this so that an event is fired even if we start observing a game
            // with an existing takeback offer (of which we're not aware).
            if ((!on) && (gameData.getTakebackOffer(player) == 0))
                gameData.setTakebackOffer(player, 1);

            updateTakebackOffer(gameData, player.getOpponent(), 0); // Remove any existing offers
            updateTakebackOffer(gameData, player, on ? 1 : 0);
            // 1 as the server doesn't tell us how many
        } else {// if (gameData.isOffered(offerId, player) != on){ this
            // We check this because we might get such an event if we start observing a game with
            // an existing offer.

            gameData.setOffer(offerId, player, on);
            listenerManager.fireGameEvent(new OfferEvent(this, game, offerId, on, player));
        }
    }


    /**
     * Updates the takeback offer in the specified game to the specified amount of
     * plies.
     */

    private void updateTakebackOffer(InternalGameData gameData, Player player, int takebackCount) {
        Game game = gameData.game;

        int oldTakeback = gameData.getTakebackOffer(player);
        if (oldTakeback != 0)
            listenerManager.fireGameEvent(new OfferEvent(this, game, false, player, oldTakeback));

        gameData.setTakebackOffer(player, takebackCount);

        if (takebackCount != 0)
            listenerManager.fireGameEvent(new OfferEvent(this, game, true, player, takebackCount));
    }


    /**
     * Accepts the given seek. Note that the given seek must be an instance generated
     * by this SeekJinConnection and it must be in the current sought list.
     */

    public void acceptSeek(Seek seek) {
        if (!seeks.contains(seek))
            throw new IllegalArgumentException("The specified seek is not on the seek list");

        sendCommand("$play " + seek.getID());
    }


    /**
     * Issues the specified seek.
     */

    public void issueSeek(UserSeek seek) {
        WildVariant variant = seek.getVariant();
        String wildName = getWildName(variant);
        if (wildName == null)
            throw new IllegalArgumentException("Unsupported variant: " + variant);

        Player color = seek.getColor();

        String seekCommand = "$seek " + seek.getTime() + " " + seek.getInc() + " " +
                (seek.isRated() ? "rated" : "unrated") + " " +
                (color == null ? "" : color.isWhite() ? "white " : "black ") +
                wildName + " " +
                (seek.isManualAccept() ? "manual " : "") +
                (seek.isFormula() ? "formula " : "") +
                (seek.getMinRating() == Integer.MIN_VALUE ? "0" : String.valueOf(seek.getMinRating())) + "-" +
                (seek.getMaxRating() == Integer.MAX_VALUE ? "9999" : String.valueOf(seek.getMaxRating())) + " ";

        sendCommand(seekCommand);
    }


    /**
     * Sends the "exit" command to the server.
     */

    public void exit() {
        sendCommand("$quit");
    }


    /**
     * Quits the specified game.
     */

    public void quitGame(Game game) {
        Object id = game.getID();
        switch (game.getGameType()) {
            case Game.MY_GAME:
                if (game.isPlayed())
                    sendCommand("$resign");
                else
                    sendCommand("$unexamine");
                break;
            case Game.OBSERVED_GAME:
                sendCommand("$unobserve " + id);
                break;
            case Game.ISOLATED_BOARD:
                break;
        }
    }


    /**
     * Makes the given move in the given game.
     */

    public void makeMove(Game game, Move move) {
        Enumeration gamesDataEnum = ongoingGamesData.elements();
        boolean ourGame = false;
        while (gamesDataEnum.hasMoreElements()) {
            InternalGameData gameData = (InternalGameData) gamesDataEnum.nextElement();
            if (gameData.game == game) {
                ourGame = true;
                break;
            }
        }

        if (!ourGame)
            throw new IllegalArgumentException("The specified Game object was not created by this JinConnection or the game has ended.");

        sendCommand(moveToString(game, move));

        Vector unechoedGameMoves = (Vector) unechoedMoves.get(game);
        if (unechoedGameMoves == null) {
            unechoedGameMoves = new Vector(2);
            unechoedMoves.put(game, unechoedGameMoves);
        }
        unechoedGameMoves.addElement(move);
    }


    /**
     * Converts the given move into a string we can send to the server.
     * Throws an <code>IllegalArgumentException</code> if the move is not of a
     * type that we know how to send to the server.
     */

    private static String moveToString(Game game, Move move) throws IllegalArgumentException {
        WildVariant variant = game.getVariant();
        if (move instanceof ChessMove) {
            ChessMove cmove = (ChessMove) move;
            if (cmove.isShortCastling())
                return "O-O";
            else if (cmove.isLongCastling())
                return "O-O-O";

            String s = cmove.getStartingSquare().toString() + cmove.getEndingSquare().toString();
            if (cmove.isPromotion())
                return s + "=" + variant.pieceToString(cmove.getPromotionTarget());
            else
                return s;
        } else
            throw new IllegalArgumentException("Unsupported Move type: " + move.getClass());
    }


    /**
     * Resigns the given game. The given game must be a played game and of type
     * Game.MY_GAME.
     */

    public void resign(Game game) {
        checkGameMineAndPlayed(game);

        sendCommand("$resign");
    }


    /**
     * Sends a request to draw the given game. The given game must be a played
     * game and of type Game.MY_GAME.
     */

    public void requestDraw(Game game) {
        checkGameMineAndPlayed(game);

        sendCommand("$draw");
    }


    /**
     * Returns <code>true</code>.
     */

    public boolean isAbortSupported() {
        return true;
    }


    /**
     * Sends a request to abort the given game. The given game must be a played
     * game and of type Game.MY_GAME.
     */

    public void requestAbort(Game game) {
        checkGameMineAndPlayed(game);

        sendCommand("$abort");
    }


    /**
     * Returns <code>true</code>.
     */

    public boolean isAdjournSupported() {
        return true;
    }


    /**
     * Sends a request to adjourn the given game. The given game must be a played
     * game and of type Game.MY_GAME.
     */

    public void requestAdjourn(Game game) {
        checkGameMineAndPlayed(game);

        sendCommand("$adjourn");
    }


    /**
     * Returns <code>true</code>.
     */

    public boolean isTakebackSupported() {
        return true;
    }


    /**
     * Sends "takeback 1" to the server.
     */

    public void requestTakeback(Game game) {
        checkGameMineAndPlayed(game);

        sendCommand("$takeback 1");
    }


    /**
     * Returns <code>true</code>.
     */

    public boolean isMultipleTakebackSupported() {
        return true;
    }


    /**
     * Sends "takeback plyCount" to the server.
     */

    public void requestTakeback(Game game, int plyCount) {
        checkGameMineAndPlayed(game);

        if (plyCount < 1)
            throw new IllegalArgumentException("Illegal ply count: " + plyCount);

        sendCommand("$takeback " + plyCount);
    }


    /**
     * Goes back the given amount of plies in the given game. If the given amount
     * of plies is bigger than the amount of plies since the beginning of the game,
     * goes to the beginning of the game.
     */

    public void goBackward(Game game, int plyCount) {
        checkGameMineAndExamined(game);

        if (plyCount < 1)
            throw new IllegalArgumentException("Illegal ply count: " + plyCount);

        sendCommand("$backward " + plyCount);
    }


    /**
     * Goes forward the given amount of plies in the given game. If the given amount
     * of plies is bigger than the amount of plies remaining until the end of the
     * game, goes to the end of the game.
     */

    public void goForward(Game game, int plyCount) {
        checkGameMineAndExamined(game);

        if (plyCount < 1)
            throw new IllegalArgumentException("Illegal ply count: " + plyCount);

        sendCommand("$forward " + plyCount);
    }


    /**
     * Goes to the beginning of the given game.
     */

    public void goToBeginning(Game game) {
        checkGameMineAndExamined(game);

        sendCommand("$backward 999");
    }


    /**
     * Goes to the end of the given game.
     */

    public void goToEnd(Game game) {
        checkGameMineAndExamined(game);

        sendCommand("$forward 999");
    }


    /**
     * Throws an IllegalArgumentException if the given Game is not of type
     * Game.MY_GAME or is not a played game. Otherwise, simply returns.
     */

    private void checkGameMineAndPlayed(Game game) {
        if ((game.getGameType() != Game.MY_GAME) || (!game.isPlayed()))
            throw new IllegalArgumentException("The given game must be of type Game.MY_GAME and a played one");
    }


    /**
     * Throws an IllegalArgumentException if the given Game is not of type
     * Game.MY_GAME or is a played game. Otherwise, simply returns.
     */

    private void checkGameMineAndExamined(Game game) {
        if ((game.getGameType() != Game.MY_GAME) || game.isPlayed())
            throw new IllegalArgumentException("The given game must be of type Game.MY_GAME and an examined one");
    }


    /**
     * Sends the "help" command to the server.
     */

    public void showServerHelp() {
        sendCommand("$help");
    }


    /**
     * Sends the specified question string to channel 1.
     */

    public void sendHelpQuestion(String question) {
        sendCommand("$tell 1 [" + Jin.getInstance().getAppName() + " " + Jin.getInstance().getAppVersion() + "] " + question);
    }


    /**
     * Overrides ChessclubConnection.execRunnable(Runnable) to execute the
     * runnable on the AWT thread using SwingUtilities.invokeLater(Runnable),
     * since this class is meant to be used by Jin, a graphical interface using
     * Swing.
     *
     * @see ChessclubConnection#execRunnable(Runnable)
     * @see SwingUtilities.invokeLater(Runnable)
     */

    public void execRunnable(Runnable runnable) {
        SwingUtilities.invokeLater(runnable);
    }


}
